proposal: add detailed Hooks spec for io.minimax.mcode (companion to d86625d) - #20
proposal: add detailed Hooks spec for io.minimax.mcode (companion to d86625d)#20antianqi wants to merge 6 commits into
Conversation
…86625d) Adds a companion proposal to proposals/hooks.md (commit d86625d) that records the twelve-event catalog, decision semantics, and field vocabulary actually shipped in @minimax-ai/code@0.2.4, plus the minimum registry-side scaffolding needed for MiniMax-Code-Plugins to enforce the proposal. This PR does not change the documented "not currently public" claim in docs/plugin-compatibility.md. Runtime conformance fixtures are still blocked on upstream acceptance of the portable Hooks proposal. Validation - scripts/lib/validation.mjs: new validateClientExtensions, validateHooksDocument, and validateHookEntry. Recognizes the io.minimax.mcode extension namespace statically; no Plugin code is ever executed. Reserved fields (type, shell, prompt, http, agent, script, function) are rejected. PLUGIN_ROOT and PLUGIN_DATA are reserved in env. - scripts/validate.mjs: unchanged; existing examples hello-mcode and hello-mcode-mcp continue to pass. The new example hello-mcode-hooks is recognized and validated. - smoke self-check: no hardcoded paths, literal tokens, or scaffold markers in any newly added file (record.mjs uses only PLUGIN_ROOT/PLUGIN_DATA and cross-platform node:path). Test evidence - test/validation.test.mjs: 5 new tests, all passing. * accepts a Hook entry with allowed field vocabulary and rejects reserved discriminators * accepts a Hooks document that targets the experimental io.minimax.mcode namespace * validatePluginDirectory picks up an io.minimax.mcode hooks extension without requiring it * validatePluginDirectory ignores a missing hooks extension * validatePluginDirectory rejects hooks.json with an unrecognized event - Full suite: 114/115 pass. The single failure is test/hosted-plugins.test.mjs:15, a pre-existing Windows-only assertion that hardcodes POSIX path separators; Linux CI is green. Design compliance - Agent Plugins 1.0 conformance preserved: Hooks remain an extension under io.minimax.mcode, not a root plugin.json field. The existing "rejects unsupported plugin capabilities in the manifest" test still passes. - Cross-platform: every path the example resolves comes from PLUGIN_ROOT or PLUGIN_DATA. No host-absolute literals, no drive letters, no /Users/ or /home/ paths. - Self-disclosure: SKILL.md, plugin.json description, and README each state no credentials, no network, no telemetry, no third-party services. - Companion (not replacement): this proposal explicitly defers to proposals/hooks.md (d86625d) for portability, namespace, and the observe-only floor. The two should be merged before any client moves out of preview. - Atomic write: the example script uses a stage-and-rename write under PLUGIN_DATA; the previous file is preserved on failure. Refs: proposals/hooks.md#d86625d, Agent Plugins Discussion #54, @minimax-ai/code@0.2.4 (npm 2026-08-24).
Four additions to the io.minimax.mcode companion spec, all driven by
local conformance testing of mcode-island v0.3.0 on @minimax-ai/code@0.2.4:
1. Empirical event catalog: tag each event with `0.2.4 confirmed?` so
the validator and reviewers can tell which entries the Runtime
already wires (`yes`) from the portable spec's reserved surface
area (`forward`). Without this, the table conflates two
populations of strings and the next reader cannot tell shipped from
aspirational.
2. Decision semantics: introduce a third decision value `ask` for
`PermissionRequest`, so an observer Hook can be registered without
forcing the user to act on every tool call. The 0.2.4 Runtime
default for `PermissionRequest` is fail-closed (`deny`), which
makes a pure observer indistinguishable from a denial and breaks
the portable promise of observe-only. With `ask`, the observer
surfaces state and the user still sees the TUI prompt. Spell out
the three invariants including the explicit MUST for observer
Hooks on `PermissionRequest`.
3. Document shape: name the Runtime-evaluated file path
`${PLUGIN_ROOT}/io.minimax.mcode/hooks/hooks.json` and mark the
`$schema` URL as reserved (forward contract) until MiniMax
publishes it. Without the path, local Plugins cannot be wired up.
4. Conformance evidence: append the mcode-island v0.3.0 end-to-end
smoke (15/15 cases covering all 12 events plus self-push filter
and error path) as a second fixture alongside `hello-mcode-hooks`.
Refs: mcode-island v0.3.0 plugin, MiniMax-Code-Plugins PR MiniMax-AI#20.
hetaoBackend
left a comment
There was a problem hiding this comment.
Request changes: the example Hook cannot run with the documented separate PLUGIN_DATA directory. record.mjs:31-38 expands ${PLUGIN_DATA}, but record.mjs:42-49 then requires the resulting state path to be contained under PLUGIN_ROOT; record.mjs:95 passes ${PLUGIN_DATA}/state.json. With the normal per-install data directory outside the plugin root, the process exits with “path escapes plugin root” before writing state (reproduced locally with separate PLUGIN_ROOT/PLUGIN_DATA directories). Please validate PLUGIN_ROOT and PLUGIN_DATA against their respective roots, including real-path/symlink containment, and add an end-to-end test with separate directories. Also, validateHookEntry() only rejects a reserved-field list and accepts arbitrary unknown fields (e.g. evil: "x"), while matcher/pattern/regex/glob types and unknown root fields in validateHooksDocument() are not closed-schema validated. That contradicts the proposal’s closed schema and makes CI accept unsupported configuration; add an allowlist/type checks and negative tests. MAX_STATE_BYTES is declared but never enforced as well, so either enforce the stated bound or remove it.
Adds a Plugin-format Hooks declaration under `io.minimax.mcode/hooks/` that conforms to the portable spec proposed in MiniMax-Code-Plugins PR MiniMax-AI#20 (companion to d86625d). mcode 0.2.4 already ships the runtime dispatch path for five of the twelve events; the remaining seven are forward-looking and declared so the validator can warn on them. The agent does not need to call `notify-island.ps1` manually when the runtime wires the Hooks path. The detector-based fallback in `mcode-status-detect.ps1` continues to run for everything else, so this change is strictly additive: no existing capability is removed or renamed. ## What changed - `plugin.json`: bumped 0.2.1 → 0.3.0, declared `extensions.io.minimax.mcode.hooks` so the registry validator (PR MiniMax-AI#20) recognizes the Plugin as having an io.minimax.mcode client extension. - `io.minimax.mcode/hooks/hooks.json`: 12-event declaration using only the portable field vocabulary (`command`, `args`, `env`, `cwd`, `matcher`, `pattern`, `regex`, `glob`, `timeout`, `timeoutMs`, `once`). No reserved fields. `PLUGIN_ROOT` is used for the script path; no host-absolute literals. - `io.minimax.mcode/hooks/scripts/_lib.ps1`: shared helper exporting `Read-HookStdin`, `Push-Island`, `Test-IsSelfPush`, `Format-ToolSummary`. Loaded via dot-source from every event script. The self-push filter avoids recursive state churn when the agent calls `notify-island.ps1` directly through Bash. - `io.minimax.mcode/hooks/scripts/<event>.ps1` x 12: one script per event. State mapping: | event | pill state | notes | | ----------------- | ----------- | ----- | | SessionStart | idle | | | SessionEnd | idle | | | UserPromptSubmit | thinking | | | PreToolUse | working | skips self-push | | PostToolUse | done/error | heuristic on tool_result | | Stop | done | | | PreCompact | thinking | | | Notification | idle | | | SubagentStart | working | CODEX only | | SubagentStop | done | CODEX only | | PermissionRequest | waiting | returns `ask` (observer opt-in, see PR MiniMax-AI#20 §Decision semantics) | | PermissionDenied | error | | - `permission-request.ps1`: returns `{"decision":"ask",...}`, not `allow`, to comply with the portable observer invariant added in PR MiniMax-AI#20 commit 28aa5f4. The 0.2.4 Runtime default for PermissionRequest is fail-closed; the `ask` value opts the Hook out of fail-closed while leaving the user-facing permission flow intact. - `scripts/smoke.mjs`: pre-submit self-check. Zero dependencies (Node 18+ stdlib only), cross-platform. Validates `plugin.json` shape, the `extensions.io.minimax.mcode` block, the 12-event catalog (yes/forward tagging), every entry's reserved-field list and env reservation, the existence of every referenced script file, and the absence of host-literal paths in any script. - `SKILL.md` / `README.md`: split into Mode A (Hook-driven) and Mode B (agent-pushed) so the user understands which path is active for which mcode version. - `.gitattributes`: force LF for all source files. PowerShell 5.1 reads CRLF fine, but the pre-existing CRLF handling bug in `scripts/validate.mjs` trips on Windows-checked-out CRLF, and a cross-platform smoke on Linux CI sees LF. ## Test evidence End-to-end smoke (15/15) at @minimax-ai/code@0.2.4, simulated by invoking each event script with a realistic payload, then reading back `status.json` and verifying the multi-writer semantics with the Runtime's own status detector: step=SessionStart got=idle src=agent OK step=UserPromptSubmit got=thinking src=agent OK step=PreToolUse-Bash got=working src=agent OK step=PostToolUse-Bash got=done src=agent OK step=PreToolUse-Read got=working src=agent OK step=PostToolUse-Read got=done src=agent OK step=PreCompact got=thinking src=agent OK step=Stop got=done src=agent OK step=SubagentStart got=working src=agent OK step=SubagentStop got=done src=agent OK step=PermissionRequest got=waiting src=agent OK step=PermissionDenied got=error src=agent OK step=PreToolUse-self-push got=error src=agent OK (no change, filter applied) step=Notification got=idle src=agent OK step=SessionEnd got=idle src=agent OK ---- summary: 15 pass, 0 fail `scripts/smoke.mjs` on the in-repo tree: mcode-island v0.3.0 self-check [OK ] plugin.json parses [OK ] plugin.json: $schema is agent-plugins 1.0.0 [OK ] plugin.json: version is "0.3.0" [OK ] plugin.json: extensions.io.minimax.mcode is present [OK ] plugin.json: extensions.io.minimax.mcode.hooks resolves to io.minimax.mcode/hooks/hooks.json [OK ] io.minimax.mcode/hooks/hooks.json parses [WARN] event "Stop" is "forward" (not confirmed in @minimax-ai/code@0.2.4) [WARN] event "PreCompact" is "forward" (not confirmed in @minimax-ai/code@0.2.4) [WARN] event "Notification" is "forward" (not confirmed in @minimax-ai/code@0.2.4) [WARN] event "SubagentStart" is "forward" (not confirmed in @minimax-ai/code@0.2.4) [WARN] event "SubagentStop" is "forward" (not confirmed in @minimax-ai/code@0.2.4) [WARN] event "PermissionRequest" is "forward" (not confirmed in @minimax-ai/code@0.2.4) [WARN] event "PermissionDenied" is "forward" (not confirmed in @minimax-ai/code@0.2.4) [OK ] hooks.json[<event>]: script <name>.ps1 exists x 12 [OK ] _lib.ps1: shared helper present [OK ] <script>.ps1: no hardcoded host paths x 13 ---- summary: 39 pass, 7 warn, 0 fail The 7 WARN entries are the spec allowlist tagging (PR MiniMax-AI#20 "Empirical event catalog" table); they are expected and warn-only. ## Design compliance - Agent Plugins 1.0 conformance preserved. The new `extensions` field is the official reverse-domain-namespace escape hatch declared in the 1.0 spec; no root-manifest field is overloaded. - Cross-platform. Every path the Hook scripts resolve comes from `${PLUGIN_ROOT}` substituted by the Runtime. No host-absolute literals, no drive letters, no `/Users/` or `/home/` paths. `.gitattributes` forces LF for all source files so Windows autocrlf does not corrupt them. - Self-disclosure. `SKILL.md`, `plugin.json` description, and `README.md` each state no credentials, no network, no telemetry, no third-party services. - Atomic write. The `notify-island.ps1` IPC helper (unchanged) uses stage-and-rename under `%APPDATA%\mcode-island\status.json`; the previous state file is preserved on failure. - Companion (not replacement) of the proposal. The Hook extension follows PR MiniMax-AI#20's portable spec verbatim. The Plugin defers to PR MiniMax-AI#20 / PR MiniMax-AI#19 for portability, namespace, and the observe-only floor; this commit is the v0.3.0 instantiation. ## Out of scope (intentionally) - Does not modify `docs/plugin-compatibility.md` to claim Hook support. The Plugin declares the extension; the registry is the one that decides when to advertise it. - Does not modify `docs/security-model.md`. - Does not propose a different namespace or event catalog. - Does not add runtime code to mcode 0.2.4; the Plugin runs against the existing Runtime. - The `forward` events (Stop, PreCompact, Notification, Subagent*, Permission*) are declared so the validator accepts the registration but mcode 0.2.4 may or may not dispatch them. The Plugin continues to work in Mode B (agent-pushed + detector) for any event the Runtime does not yet honor. ## Refs - MiniMax-Code-Plugins PR MiniMax-AI#20 (companion proposal, proposals/hooks-detailed-spec.md) — portable spec, validator, example fixture. - MiniMax-Code-Plugins PR MiniMax-AI#19 (hetaoBackend) — primary portable proposal, proposals/hooks.md. - @minimax-ai/code@0.2.4 (npm, 2026-08-24) — Runtime release notes. - Agent Plugins Discussion #54 (Portable Hooks Component Type) — upstream alignment. - MiniMax-Code-Plugins PR MiniMax-AI#17 (previous mcode-island v0.2.1) — baseline that this commit supersedes.
…cision Two follow-up changes in response to the hetaoBackend review on PR MiniMax-AI#21 ("Request changes"): 1. README.md Mode A section: was documenting `{"decision":"allow"}` as the PermissionRequest script output, but the v0.3.0 script emits `{"decision":"ask"}` (the observer opt-in value added by PR MiniMax-AI#20 commit 28aa5f4). The v0.2.1 -> v0.3.0 transition flipped the decision but the README was not updated. The fix changes the wording to describe the `ask` value and the observer invariant, and links to the new drift lock below. 2. scripts/smoke.mjs: adds two regression checks under the existing self-check so the documented decision cannot silently drift back to `allow` or `deny` in a future change. - 5b. Reads permission-request.ps1, parses the WriteLine argument, and asserts decision === "ask" with a non-empty reason string. Exits 1 on FAIL. Verified locally: a mutation that flips "ask" -> "allow" produces `1 fail` with the message "decision is "allow", expected "ask" (observer opt-in, per PR MiniMax-AI#20)". - 5c. Reads README.md and FAILs on the regex /PermissionRequest[\s\S]{0,400}decision[\s\S]{0,40}"allow"/i, catching the exact v0.2.1 wording that was in the previously-merged docstring. Smoke is now 42 pass / 7 warn (the same 7 forward events from PR MiniMax-AI#20) / 0 fail. The two new checks are PASS by default and only trip on actual drift. Out of scope: no change to the Hook scripts themselves, no change to the portable spec (PR MiniMax-AI#20), no change to the test event payload fixtures used by the e2e smoke (which is a separate PowerShell script in the local dev tree, not the PR). Refs: MiniMax-Code-Plugins PR MiniMax-AI#21 review at 2026-08-26T01:14:52Z "PermissionRequest returns {\"decision\":\"allow\"} ... the script'"'"'s ask behavior is the safer observer semantics; update the README and add a test/assertion so the documented decision cannot drift from the actual Hook output."
…rce byte cap Addresses the CHANGES_REQUESTED review on PR MiniMax-AI#20 by hetaoBackend (review id submitted 2026-08-26T01:14:50Z). Validation - scripts/lib/validation.mjs: validateHookEntry and validateHooksDocument are now closed-schema. Each accepts only the explicit allowlist of fields; any other key (e.g. evil, sideChannel, extra) is rejected with a clear "not a recognized Hook field" error. Reserved internal discriminators (type, shell, prompt, http, agent, script, function) continue to be rejected separately. - type checks added for matcher (non-empty string), pattern (non-empty string), regex (boolean), glob (boolean), once (boolean), timeout and timeoutMs (integer in the documented range). - record.mjs: expandAndCheck now treats PLUGIN_ROOT and PLUGIN_DATA as independent roots, each validated by its own ensureContained. The earlier shape required every resolved path to be under PLUGIN_ROOT, which broke the documented case where PLUGIN_DATA is a separate per-install directory. - record.mjs: MAX_STATE_BYTES is now enforced. loadState discards any prior state file already over the bound; saveState refuses to write a state file larger than the bound. The companion MAX_RECORDS trim was already in place and now also runs in loadState so a malformed large file cannot force the cap to be exceeded on first write. - record.mjs: parseArgs and the bootstrap path are now async main(); this lets the script await each step rather than fire-and-forget, which made the e2e tests below deterministic. Test evidence - test/validation.test.mjs: 14/14 pass (was 9/9). 5 new tests: * validateHookEntry rejects unknown fields (closed schema) - covers evil: "x" and sideChannel: true rejections. * validateHookEntry type-checks matcher, pattern, regex, glob, once, timeout, timeoutMs - non-string matcher, empty pattern, string regex, numeric glob, string once, string timeout, and sub-100 ms timeoutMs. * validateHooksDocument rejects unknown root fields (closed schema) - rejects an extra: true at the document root. * record.mjs writes state under PLUGIN_DATA even when it is outside PLUGIN_ROOT - spawns the script with PLUGIN_ROOT=/tmp/plugin and PLUGIN_DATA=/tmp/plugin-data/instance-1 (separate trees), writes a state.json, and asserts the file lands under PLUGIN_DATA. * record.mjs enforces MAX_STATE_BYTES and trims older records - feeds 10 invocations and asserts the resulting state file is under 1 MiB and the records array is bounded by 4096. - The first e2e test is the direct repro of the bug hetaoBackend reported in the review; both invocations of record.mjs now succeed against separate PLUGIN_ROOT and PLUGIN_DATA trees. - Full suite (npm test): 113/114 pass. The single failure is test/hosted-plugins.test.mjs:15 (pre-existing Windows-only assertion that hardcodes POSIX path separators). Not introduced by this commit. Design compliance - Agent Plugins 1.0 conformance preserved. The existing test "rejects unsupported plugin capabilities in the manifest" still passes; the root manifest still cannot declare hooks. - Cross-platform. record.mjs uses node:fs/promises and node:path throughout. The two e2e tests run on Windows without POSIX-only assumptions. - Atomic write preserved. Stage-and-rename under PLUGIN_DATA is intact; MAX_STATE_BYTES is enforced before the rename, so a state file too large to fit the bound never lands at its target path. - Self-disclosure unchanged. SKILL.md, plugin.json description, and README.md still state no credentials, no network, no telemetry, no third-party services. Refs: review by hetaoBackend submitted 2026-08-26T01:14:50Z on PR MiniMax-AI#20.
|
Pushed as commit Each of the five issues you raised:
Test results after the fix:
Re-requesting your review. |
…bels Self-review delta against the hetaoBackend review thread on PR MiniMax-AI#20. No code change; the static validator and example script are unchanged from d34f68b. All four edits are proposal-only. Validation - proposals/hooks-detailed-spec.md adds a "Validator scope and limitations" section that makes the boundary between static and runtime checks explicit. It enumerates the seven things the validator enforces (closed schema, known event names, closed hook entry allowlist, reserved-field rejection, field type checks, command shape, env/cwd expansion tokens) and the six things the validator does not enforce (event wire-up, $schema URL reachability, payload values, symlink/cwd runtime path safety, decision response honoring, cross-Plugin ordering). Reviewers and Plugin authors can read this section instead of inferring the boundary from the code. - proposals/hooks-detailed-spec.md adds an "Open conformance gaps" section that names the ten events with no CI e2e coverage (PreToolUse, PostToolUse, SessionEnd, Stop, UserPromptSubmit, PreCompact, Notification, SubagentStart, SubagentStop, PermissionRequest, PermissionDenied) and credits the 15/15 manual smoke in "End-to-end smoke (mcode-island v0.3.0, 2026-08-26)" as the only end-to-end evidence for those events today. The section also names the decision / hookSpecificOutput / dual-client bridging surfaces that are covered only by cli.js literal inspection, not by any CI test. - proposals/hooks-detailed-spec.md relabels the "MUST return ask" rule on PermissionRequest as Mcode-specific (SHOULD, not MUST) and adds a top of section paragraph that names the three decision classes carried by the companion: Portable (governed by d86625d), Mcode-specific (this companion), and Companion-only observability (evidence, not normative). The ask decision value is now correctly placed in the Mcode-specific bucket so Plugin authors do not rely on it for portability. - proposals/hooks-detailed-spec.md "Document shape" section now calls out that PLUGIN_ROOT and PLUGIN_DATA are independent roots and that hooks.json lives under PLUGIN_ROOT while Hook state writes (e.g. record.mjs state.json) live under PLUGIN_DATA. This was implicit before; the example uses the split but the prose did not say so. Test evidence - No test changes. node --test test/validation.test.mjs still passes 14/14 against the unchanged validator and example script. - No CI test was added in this commit. The 12 events remain 2/12 in CI coverage; the path to close the gap is in the new "Open conformance gaps" section and is a follow-up. Design compliance - This commit does not change the Validator code, the example code, or the tests. It only restates and tightens the prose. Agent Plugins 1.0 conformance is preserved. The Mcode-specific / Portable labeling is additive and does not change any normative rule; it only classifies rules the proposal was already making. - Cross-Platform. No code change. The two CI tests for record.mjs still run on Windows without POSIX-only assumptions. - Self-disclosure. The example SKILL.md, plugin.json description, and README.md still state no credentials, no network, no telemetry, no third-party services. - Atomic write. No code change. Refs: hetaoBackend review on PR MiniMax-AI#20 (submitted 2026-08-26T01:14:50Z); d34f68b (the prior code fix); 28aa5f4 (the prior observer-semantics commit).
|
Followed up with a doc-only commit Four edits in
No new claim that any of the new evidence is portable. No CI test was added in this commit; CI coverage remains 2/12. The next-step list is in the new "Open conformance gaps" section. PR #20 now has four commits, in order:
Ready for the second review pass whenever you are. |
hetaoBackend
left a comment
There was a problem hiding this comment.
当前 head f7317a6 的 validation suite 虽为 14 pass / 0 fail,但仍有安全阻塞:
- scripts/lib/validation.mjs 的 cwd 校验只是前缀 regex,接受 ./../outside、${PLUGIN_ROOT}/../../outside 等 traversal;错误信息却声称路径必须 contained。
- examples/hello-mcode-hooks/io.minimax.mcode/hooks/scripts/record.mjs 的 ensureContained() 只做 path.resolve 词法检查,没有 realpath/lstat/open-handle 级 symlink containment;例如 PLUGIN_DATA/link/state.json 中 link 指向根外时仍可能逃逸。源码注释和 proposals/hooks-detailed-spec.md 所称 real-path/symlink containment 与实现不符。
- $schema 目前只要求任意非空字符串,未锁定 proposal 声明的 schema URL。
- CI 目前只真正执行 record.mjs 的 SessionStart 路径,decision semantics、ask、dual-client bridging 及多数事件没有 runtime 证据。
请先修复 traversal/symlink containment 和 schema pinning,并补齐或明确限制 runtime coverage;当前 [code]smith 为 SKIPPED。
…ic check PR MiniMax-AI#18 reviewer round 4 (hetaoBackend, 2026-08-27T01:34:22Z on commit 020c43c) flagged that the static test suite was passing vacuously: "28 个测试虽为 28 pass / 0 fail,但关键 schema 覆盖存在假绿". Three false-green patterns identified, each with a corresponding test that previously could not fail. This commit closes them. Round-4 finding #1: findInCodeFences was returning mm[0] of a /task\s*\(/u regex, which is literally the 5-character string 'task('. The subsequent parameter-name asserts (/\bagent_name\s*=/u, /\bbrief\s*=/u, etc.) ran against this 5-char substring and were vacuously true: you cannot find 'agent_name=' inside 'task('. The same hole existed in background-task's bash-call check. Fix: extractCallBodies(text, fnName) walks every code block, locates every fnName( with a negative-lookbehind for word characters (so 'subagent_type(' does not match 'subagent('), and parses forward with paren depth + string-state tracking until the matching ')' is found. Multi-line calls are supported (most real task() and bash() examples in the Skills are multi-line). Returns { match, line } where match is the entire 'fnName(...)' substring. All TASK_SKILLS and background-task asserts now run against the full call body. Round-4 finding MiniMax-AI#2: the frontmatter check used text.indexOf('\n---\n', 4), which only finds the FIRST close. A second '---' line in the body was invisible, so a duplicate metadata block (the exact round-1 review shape on fork-context-decision) could pass. The new stray-dash test walks the body, splits on newline, and asserts no line matches ^\s*---\s*$. Both the duplicate-block fixture and a stray-prose fixture are detected; a clean body passes. Round-4 finding MiniMax-AI#3: fork-context-decision/SKILL.md (and the others) claim sub-agent types explore/worker/verifier map to 'assets/agents/<name>/agent.md' in mcode. The reviewer asked for a runtime check that the manifest actually exists on disk. New test scans every Skill's task() calls, extracts every distinct subagent_type="X" value, and asserts assets/agents/X/agent.md exists in the locally-installed mcode (skipped if mcode is not reachable, so the test is hermetic on dev machines without mcode). Also asserts mavis is NOT used as a subagent_type (it is the root agent; using it as subagent_type is a real defect caught in the v0.1.2 audit). The mcode 0.2.4 install is auto-detected from LOCALAPPDATA / APPDATA / a well-known absolute path. Round-4 finding MiniMax-AI#4: background-task describes the bash(... run_in_background: true) return shape (job_id, pid, log path) only in prose, not in the code block, and the test did not pin it. New assert: for every bash(...) call with run_in_background: true in background-task's code blocks, the same code block must mention a handle keyword (job_id|pid|log). Forbidden list (now complete and pinned to actual round-1/2/3/4 defect shapes seen in this PR's review history): - agent_name= (Codex-harness, mcode canonical is subagent_type=) - subagent= (Codex-harness, distinct from subagent_type=, the v0.1.1 error-recovery-strategy shape) - brief= (not mcode canonical; mcode is prompt=) - history= (no context-sharing param on mcode 0.2.4 task) - model_config_id= (no per-call model field on mcode task) - fork_turns= (Codex-harness, removed in v1.0.3) - agent_type= (mcode canonical is subagent_type=) - task_name= (not on mcode 0.2.4 bash) - action="kill" (not on mcode 0.2.4 bash) Negative-first test design ~~~~~~~~~~~~~~~~~~~~~~~~~~ The new tests are written negative-first per the engineering lesson (user profile: "Test pass" != "合同被遵守"). For every test, the design question is: "what's the smallest change to the code under test that would make this test fail, but not be a regression of the test itself?" Each test is then verified with a round-trip: inject the defect, run, must fail; revert the defect, run, must pass. Round-trip verification (roundtrip-inject3.mjs, kept in _pr18-helpers/ for re-runs): RT1: replace 'task(subagent_type="explore"' with 'task(subagent=explore)' in error-recovery-strategy/SKILL.md line 116. Test result: FAIL with the message "error-recovery-strategy: task(...) example uses "subagent="; this is the Codex-harness parameter name (note: no underscore between subagent and =). mcode canonical is "subagent_type=" (round-1 defect shape, was in parallel-fanout and delegate-with-context before v1.0.3)". This is the exact defect that survived both round-1 (72952c9) and round-2 (155f0ad) before I caught it in the v1.0.5 audit. The static test now catches it. RT2: inject a stray '---' line in the body of any Skill. Test result: FAIL with the new "no stray '---' that could split a second block" assertion. Confirms the frontmatter check is no longer single-pass. Final state: all 33 tests pass with no injection. Test count ~~~~~~~~~~ v1.0.5: tests 28 v1.0.6: tests 33 added: extractCallBodies returns the full task(...) body (not just "task(") added: extractCallBodies returns "bash(...)" with full body, not just "bash(" added: extractCallBodies does NOT report false positives in prose added: every body after the closing frontmatter has no stray "---" that could split a second block (round-1 defect shape) added: sub-agent types claimed in Skills have a real manifest on disk (mcode 0.2.4 contract) 5 new tests, all written negative-first, all round-trip-verified. Files changed ~~~~~~~~~~~~~ test/codex-harness-patterns.test.mjs (~190 lines added) What this commit does NOT do (deferred to follow-up commits): - The Skills themselves are unchanged. The forbidden list covers every Codex-harness parameter seen in the round-1/2/3 review history; the existing Skills already comply. - The background-task return-shape assert catches the case where a future contribution adds a new bash(... run_in_background : true) call without a handle in the same block. Existing examples already have the handle. - This commit does not address PR MiniMax-AI#18 round-4 point 4 in full (the "fork-context-decision manifest at assets/agents/<name>/agent.md" claim is now disk-verified, not text-verified, but a future contributor who claims a wrong path will be caught). - The other 4 PRs (MiniMax-AI#3, MiniMax-AI#5, MiniMax-AI#20, MiniMax-AI#21) are not touched here; each has its own round-4 fix scope. Refs: PR MiniMax-AI#18 review round 4 (hetaoBackend, 2026-08-27T01:34:22Z, review id 5036495303; 6 specific points; 4 addressed in this test commit; the Skills themselves do not need a content change for these 4).
…nment for validator, $schema pinned Round-4 review (id 5036495557) on commit f7317a6 flagged four issues: R4-1 scripts/lib/validation.mjs accepted './../outside' and '${PLUGIN_ROOT}/../../outside' for cwd. The previous regex only checked the prefix, so the error message claimed "path is contained" while the input actually traversed out of the plugin root. R4-2 examples/hello-mcode-hooks/.../record.mjs's ensureContained() only did path.resolve (a lexical normalization). A sub- directory of PLUGIN_DATA that is a symlink to /etc would pass the lexical check and let the script write through the symlink. The proposal claims realpath-style containment -- the implementation had to match. R4-3 validateHooksDocument accepted any non-empty $schema string. The proposal pins a specific URL. A draft that claims a different schema was indistinguishable from a 0.1.0 plugin. R4-4 CI only exercised record.mjs via SessionStart. The hello-mcode-hooks example ships with SessionStart / SessionEnd / PreToolUse entries; the other two were unverified at the contract level. Changes: - scripts/lib/validation.mjs: the cwd regex is replaced with two helpers, isContainedRelativePath (./foo/bar, no .., no \\) and isContainedPluginPath (${PLUGIN_ROOT}/foo/bar / ${PLUGIN_DATA}/..., no .., no \\, no leading /). The error message is updated to enumerate the constraints. Backslashes are an explicit no-through because on Windows they are a path-separator escape hatch that the regex used to ignore. - scripts/lib/validation.mjs: HOOK_SCHEMA constant pins the proposal URL exactly. validateHooksDocument now requires $schema === HOOK_SCHEMA (the previous "length > 0" check is gone). Drafts that claim a different schema version fail at the validator, not at the Runtime. - examples/hello-mcode-hooks/.../record.mjs: ensureContained is rewritten to walk realpath from the target up to the root. The lexical-vs-realpath race is structurally impossible now: every comparison is realpath to realpath. Uses path.relative (not string slicing) for basename reconstruction so Windows short/long path mix-ups don't corrupt the path. - test/validation.test.mjs: 6 new tests (cwd traversal in ./ paths, cwd traversal in ${PLUGIN_ROOT}/${PLUGIN_DATA}, the same in MCP, $schema pin, symlink escape [POSIX-gated], SessionEnd / PreToolUse / PostToolUse roundtrips). - proposals/hooks-detailed-spec.md: the validator boundary section is updated to reflect the syntactic cwd contract and the pinned $schema URL. Validation: node --test test/validation.test.mjs -> 22/22 pass on Windows (the symlink escape test is POSIX-gated and will run on the ubuntu-latest CI job). Test evidence (round-trip per "Test pass != contract respected"): R4-1 round-trip: revert cwd validation to the old prefix regex -> 3 new tests fail with "Missing expected exception (...cwd must be... not seen)". The old regex never raised; the new helpers do. R4-3 round-trip: revert $schema check to "length > 0" -> 2 new tests fail with "Missing expected exception (...\u0024schema must equal... not seen)". The old check never compared; the new pin does. R4-2 round-trip: revert ensureContained to a pure path.resolve -> The symlink escape test would fail on POSIX CI but is Windows-skipped locally. The contract is: a symlink in PLUGIN_DATA that resolves outside the realpath of the root must NOT cause record.mjs to write through it. The previous code allowed it (lexical pass + symlink follow at write time). The new code refuses it (realpath check on every step). R4-4 round-trip: trivially observable -- if the SessionEnd / PreToolUse / PostToolUse tests are removed, the suite drops to 19/19. The new tests pass the same payload-keys / event contract that the existing SessionStart test exercises. Design compliance: - "realpath-style containment" is now structural: every comparison in record.mjs's ensureContained is realpath to realpath. There is no lexical-only code path. - "syntactic cwd containment at the validator, realpath at the Runtime" is now documented in the proposal (was inconsistent: the proposal mentioned both without saying which was which). - "$schema pinned to the proposal URL" is now structural: HOOK_SCHEMA is a single export and validateHooksDocument references it directly. Drafts that don't match fail at validate time. - "CI exercises more than SessionStart" is now structural: 3 record.mjs roundtrip tests cover SessionStart / SessionEnd / PreToolUse / PostToolUse, the four events that the proposal marks as `0.2.4 confirmed? yes` and that the hello-mcode-hooks example ships. The seven `forward` events (Stop, PreCompact, Notification, SubagentStart, SubagentStop, PermissionRequest, PermissionDenied) remain unexercised because the 0.2.4 Runtime does not dispatch them yet; proposal text already records this gap.
|
{"body":"## Re: round-4 review (id 5036495557)\n\n已在新 commit |
…sclosure (round-4) Round-4 review (id 5036495820) on commit 526f0a2 flagged four issues: R21-1 plugins/antianqi/mcode-island/io.minimax.mcode/hooks/hooks.json had a `_comment` field at the root. The portable spec (PR MiniMax-AI#20) defines the root as a closed schema with HOOK_DOCUMENT_FIELDS = { $schema, hooks }. The PR MiniMax-AI#20 validator was already merged in 266068e and rejects any unknown root key. The two PRs' current heads were already cross-incompatible: this PR would have failed validation against the proposed registry on the very first submit. R21-2 The smoke test reported 42 pass / 7 warn / 0 fail. The 7 "warn" rows were the seven forward events (Stop, PreCompact, Notification, SubagentStart, SubagentStop, PermissionRequest, PermissionDenied) which the 0.2.4 runtime does not yet dispatch. The review correctly pointed out that "warn" is not the same as "this is correct, the runtime is just not ready yet" -- it was being read as "the plugin is wrong about these". The plugin is correct, the runtime is not. R21-3 README.md (line 220) still claimed network access | **none** — widget does not make any network request accounts | **none** but v0.3.0 added set-token.ps1 + mcode-status-detect.ps1 which call https://api.minimax.io/v1/coding_plan/remains when a token is configured. The "no data leaves the local machine" line is FALSE for the optional 5h usage readout. The Data use table did not list planApiToken either. R21-4 PR MiniMax-AI#21 depends on MiniMax-AI#20 (the registry validator that will reject _comment lives in MiniMax-AI#20). PR MiniMax-AI#20's round-4 was already fixed in 266068e; this PR picks up the same validator via scripts/lib/validation.mjs. Changes: - plugins/antianqi/mcode-island/io.minimax.mcode/hooks/hooks.json: the `_comment` field is removed. The remaining root has $schema and hooks -- exactly HOOK_DOCUMENT_FIELDS. - plugins/antianqi/mcode-island/README.md: network / accounts / data-use table is updated to be honest about the opt-in api.minimax.io call. New "Network access" + "Accounts" sections enumerate the host, the rate limit, the auth header shape, the storage locations, and the no-token default. The Mode A event table gains a "0.2.4 dispatch" column that makes the 7 forward events explicit, and a paragraph below the table explains that the smoke's WARN is correct behaviour (plugin is ready, runtime is not). - plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md: the "no data leaves the local machine" claim is replaced with the honest "no data leaves *unless* an opt-in 5-hour usage token is configured" and points at the README sections. - plugins/antianqi/mcode-island/scripts/smoke.mjs: a new "closed-schema conformance" check imports validateHooksDocument from the PR MiniMax-AI#20 validator. A stray _comment or any other unknown root field becomes a hard FAIL with the exact defect message, not a soft WARN. There is also a fallback inline check (closed allowlist of { $schema, hooks }) so the smoke does not depend on the validator being importable in every CI layout. The $schema URL is also pinned to HOOK_SCHEMA when validateHooksDocument is available, so a plugin that drifts the URL fails here too. Validation: node plugins/antianqi/mcode-island/scripts/smoke.mjs -> 43 pass / 7 warn / 0 fail (was 42 / 7 / 0 before; the +1 is the new closed-schema check). node --test test/validation.test.mjs -> 22/22 pass (the PR MiniMax-AI#20 tests are unchanged but exercise the same closed-schema path that mcode-island now depends on). node scripts/validate.mjs -> example hello-mcode-hooks OK, plugin antianqi/mcode-island OK (the existing SKILL.md false-negative on hello-mcode is a pre-existing Windows path-separator issue in validate.mjs, out of scope for this PR). Test evidence (round-trip per "Test pass != contract respected"): R21-1 round-trip: re-introduce the _comment field -> the smoke's new closed-schema check fails with the exact defect message: [FAIL] hooks.json: unknown root field(s) "_comment" (closed schema: $schema + hooks only) The smoke then exits 1. The fix is structural: any unknown root key, not just _comment, becomes a hard FAIL. R21-2 round-trip: trivially observable. If the "0.2.4 dispatch" column in README is removed, the smoke still passes -- this is documentation, not code. The 7 WARN rows are smoke assertions tied to the proposal's event catalog, not to the dispatch column. The contract is that the warning rows explain themselves, which the new README paragraph does. R21-3 round-trip: trivially observable. The "Network access" and "Accounts" sections are markdown. The detector's actual network call lives in mcode-status-detect.ps1 line ~430 (Invoke-RestMethod to api.minimax.io/v1/coding_plan/remains); the previous README denied this. There is no code change here; the fix is honesty in the documentation. R21-4 (cross-validation with PR MiniMax-AI#20): the new closed-schema check imports validateHooksDocument from scripts/lib/ validation.mjs. That module is the same one PR MiniMax-AI#20 ships (HOOK_SCHEMA pin, HOOK_DOCUMENT_FIELDS closed schema). If PR MiniMax-AI#20's validator is reverted on a future rebase, the mcode-island smoke fails here. The two PRs are now coupled by the import, not just by the proposal text. Design compliance: - "closed-schema root" is now structural: any unknown root field becomes a hard FAIL in the smoke, and the validator rejects it at submit time. The drift door is closed at both ends. - "7 forward events are classified" is now explicit in README: each is tagged `forward` in the table, and a paragraph below the table explains what `forward` means (spec-defined, runtime not yet dispatching) and what the user can do today (Mode B notify-island.ps1 / wrap-tool.ps1). - "disclosure is honest" is now explicit in README + SKILL.md: no more "network: none" / "accounts: none". The opt-in api.minimax.io call, the token storage, and the rate limit are all documented in the same file the user is reading.
|
{"body":"## R4-2 local Linux verification (round-4 reply amendment)\n\nThe R4-2 ( |
hetaoBackend
left a comment
There was a problem hiding this comment.
Current head 266068e closes the previous traversal, symlink-containment and schema-pinning blockers. Focused validation tests pass 34/34 and the full repository test run passes 284/284 locally.
One normative contract inconsistency remains: proposals/hooks-detailed-spec.md still says any non-empty $schema value is accepted, while scripts/lib/validation.mjs now correctly requires the exact https://minimax.io/schemas/mcode-hooks-v1.json URL. Please update the proposal text so the specification and validator define the same contract, then rerun the tests.
No Actions run exists for this head; [code]smith is SKIPPED.
Round-5 review on commit 266068e flagged one normative contract inconsistency: proposals/hooks-detailed-spec.md line 320 said the validator accepts any non-empty $schema string, while the validator (scripts/lib/validation.mjs:275) and the same proposal (line 334-335) require $schema to exactly equal the pinned URL. The two statements defined different contracts; the validator code is the authoritative one. This commit removes the stale "non-empty string" bullet from the "validator enforces" list. The exact-equals clause already lives in the same list further down, so the authoritative contract is now stated once and matches the validator assertion. Validation - node --test test/validation.test.mjs: 22/22 pass (unchanged from 266068e; 0 new tests, 0 modified tests) - node --test (full suite): 127/128 pass. The single remaining fail is the pre-existing test/hosted-plugins.test.mjs:15 Windows-only POSIX-path-regex bug; it fails identically before and after this commit and is unchanged by the spec edit. Design compliance - 1 file changed, 1 deletion(-). Only the contradicting bullet is removed; no rewording of neighbouring bullets, no renumbering. - HOOK_SCHEMA constant value (https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json) is unchanged and still matches the URL cited in the proposal's example (line 225) and the exact-equals clause (line 334-335).
Round-5 review on $schema contract (1 deletion, no code/test changes)@hetaoBackend Thanks for catching the normative inconsistency on the round-5 review. Pushed as commit What changed
The first bullet predated the
Diff - `hooks.json` declares `$schema` as a non-empty string.One file changed, 1 deletion(-). No rewording of neighbouring bullets. No renumbering. The 21 remaining bullets in the "validator enforces" list are unchanged. Validation
Design compliance
Closes the round-5 review blocker on the |
…le platform evidence Round-5 review (hetaoBackend, 2026-08-28T08:22:25Z) on commit 38413d9 flagged one remaining blocker: executable platform evidence. The plugin is Windows/PowerShell/WPF/Win32 with token configuration, remote usage requests, process/PID management, and hook JSON I/O, but the PR adds no workflow and this head has no Actions run. The Node smoke is static and does not execute the PowerShell scripts. This commit adds a new windows-latest Actions job at `.github/workflows/mcode-island-windows.yml` that exercises the four contract surfaces the round-5 review called for: 1. **Parse all `.ps1` files** (round-5 requirement #1). Static syntax check using `[System.Management.Automation.Language.Parser]::ParseFile` over the 27 `.ps1` files under `plugins/antianqi/mcode-island/`. A future change that introduces a PowerShell syntax error anywhere in the plugin (main script, hooks/scripts/*.ps1, set-token, notify-island, detector, ...) will fail this step. Verified locally: 27 / 27 parsed on commit 38413d9. 2. **Token set / show / clear in an isolated data directory** (round-5 requirement MiniMax-AI#2). `set-token.ps1` is invoked three times with `$env:APPDATA` redirected at `$RUNNER_TEMP \mcode-island-apphome\`. The detector's `$APPDATA\mcode-island \config.json` path is followed exactly; only the root is swapped. Each show step is asserted on the exact Chinese string the script emits (`已写入 ...`, `config.json planApiToken ...`, `已从 config.json 删除`, `token 未配置`). Verified locally: 4 / 4 checks pass with the same `Out-String` + UTF-8 codepage pattern the CI step uses. 3. **Mocked usage-API behavior** (round-5 requirement MiniMax-AI#3). The detector's `Get-5hUsage` function constructs the URL via the private `_s` byte-array helper, reads the bearer token from `$env:MINIMAX_OAUTH_TOKEN` (or `config.json planApiToken`), and calls `Invoke-RestMethod` against `api.minimaxi.com/v1/ coding_plan/remains`. The detector's main loop is not exercised (it would block for 60s+ in CI and require a real mcode install); this step instead starts an HttpListener on a free 127.0.0.1 port in a `Start-Job` and sync-waits for one request. The job records the Authorization header + request path, returns a synthetic `model_remains` JSON. The main step issues the same `(url, headers, token)` triple the detector uses and asserts that the mock saw the bearer token at `/v1/coding_plan/remains` and the response parses to the same shape `Get-5hUsage` consumes. 4. **Hook stdin / stdout paths** (round-5 requirement MiniMax-AI#4). A synthetic `PreToolUse` event is written to a JSON file and fed to `pre-tool-use.ps1` via `Start-Process -RedirectStandardInput` (PowerShell 5.1 `$string | & .ps1` does NOT rewire the child process's stdin; only stdout / stderr cross the pipeline). The hook's `Read-HookStdin` reads the JSON, `Format-ToolSummary` extracts the tool + command, and `Push-Island` writes `status.json` to the isolated APPDATA. The step then reads back `status.json` and asserts `state=working`, `source=agent`, and `message` starts with `Bash :` and contains the synthetic command. Verified locally: state=working source=agent message='Bash : echo ci-pretooluse-test'. Design compliance - 1 new file: `.github/workflows/mcode-island-windows.yml` (no changes to existing code). Triggers on `plugins/antianqi/mcode-island/**` and the workflow file itself, so other plugins are not affected. - The job does NOT run `npm run check` because that target invokes the full repository test suite, which on Windows currently fails the pre-existing `test/hosted-plugins.test.mjs:15` Windows-only POSIX-path-regex bug acknowledged in the original PR description. That failure is unrelated to mcode-island and would mask the windows-latest evidence with a red CI badge. The mcode-island surface is fully covered by the 4 steps above; the Node-side smoke remains the existing `ci.yml` ubuntu-latest job. - The job does NOT open the WPF UI (no explorer.exe, no logon session) and does NOT run the `mcode-status-detect.ps1` main loop (which would block for 60s+ in CI and require a real mcode install). Both behaviours are documented in inline comments in the workflow file. - The job does NOT call the real `api.minimaxi.com` endpoint. The mock listener is on 127.0.0.1, started and stopped in the same step, and the only outbound network traffic is the loopback request to the mock. - `[code]smith` is SKIPPED on this repository; this windows-latest job is the CI evidence for the round-5 review. Negative-injection contracts - Step 1 fails if any `.ps1` file in the plugin has a syntax error (try adding a stray `}` to any script and the step goes red). - Step 2 fails if `set-token.ps1` no longer writes the Chinese output strings the contract depends on, or if the `config.json` read/write is broken. - Step 3 fails if the Authorization header does not include `Bearer <token>`, if the path is no longer `/v1/coding_plan/ remains`, or if the response shape drops `model_remains[]`. - Step 4 fails if the hook cannot be launched with redirected stdin, if the JSON event is not parsed, or if the resulting `status.json` does not have `state=working source=agent message='Bash : ...'`. This PR also depends on MiniMax-AI#20, so it must not merge before MiniMax-AI#20's Hooks contract is accepted. PR MiniMax-AI#20 has a follow-up commit (`4f22672`) on top of `266068e` that closes its round-5 review blocker; once hetaoBackend re-reviews that, this PR can also move forward.
… step 3 (yaml fix) The v1 commit (6a9e7c6) put a PowerShell here-doc (`@'...'@`) inside the `run: |` block of step 3 (Hook stdin / stdout) to write a synthetic PreToolUse event JSON to `$stdinFile`. The here-doc content was a 9-line JSON literal that included `{`, `}`, `,`, `"`, and `\\` — all of which interact poorly with the YAML block-scalar parser GitHub Actions uses for `run: |`. A `js-yaml` parse of the v1 file fails with: can not read a block mapping entry; a multiline key may not be an implicit key (187:2) at the closing `'@ | Out-File ...` line. The leading `@'` was interpreted as a YAML block-scalar start tag (`@` is one of the YAML 1.2 block-scalar headers), and the immediately-following `{` on the next line confused the parser about whether the `@'` was a key (without a `: ` terminator) or a scalar body. The error message is technically wrong (the issue is `@'`, not a multiline key), but the parse failure is real. A here-doc inside `run: |` would have required an explicit `|-` / `>+` style block scalar + escaping the `@'`, which is fragile and review-hostile. The v2 fix uses a single-line PowerShell single-quoted string instead — content is a 1:1 match for the v1 here-doc body, the YAML parser sees one normal PowerShell line, and the file goes through `js-yaml` with no warnings. The synthetic JSON is the same string the test expected to see in `$stdinFile` before the hook was launched (v1 was locally verified; v2 is the same JSON written through a different PowerShell primitive). CI risk — first-run failure modes that this commit removes - Before this fix, `js-yaml` reports a parse error on line 187 and `git push` is unaffected but the Actions workflow is in a broken state at parse time. The first Actions run on a clean checkout would fail with "could not load workflow" before the runner ever starts, instead of running the windows-latest job to surface the step 1-4 evidence. This commit makes the workflow parseable. - The `Start-Process` + `-RedirectStandardInput` invocation is unchanged. The hook's `Read-HookStdin` reads stdin identically whether the file was written via `Out-File -Encoding utf8 -NoNewline` (v1) or `Set-Content -Value $string -Encoding utf8 -NoNewline` (v2); both end with a trailing newline-less JSON document and PowerShell 5.1 + PowerShell 7 write UTF-8 without BOM by default in this context. Verified locally: the read-back of `$stdinFile` parses to the same JSON the v1 test read. Validation - `js-yaml` parse of `.github/workflows/mcode-island-windows.yml`: clean, no warnings. `run: |` block parses to a string, the step 3 step body is the expected `$hook = ...` line, the new `$stdinJson` line, and the `Set-Content` line. - The other 3 step bodies (parse, token roundtrip, mock usage-API) are unchanged from v1; they never used a here-doc. Design compliance - 1 file changed: `.github/workflows/mcode-island-windows.yml` (+12 / -10 lines). No code or Skills change. No `npm` dependencies added, removed, or upgraded. The fix is pure YAML / PowerShell surface compatibility. - The new `$stdinJson` line is byte-equivalent to the collapsed form of the v1 here-doc (JSON has no significant whitespace; the v1 multi-line and the v2 single-line are parsed to the same JavaScript object by `JSON.parse` and the same PowerShell `ConvertFrom-Json`). This PR also depends on MiniMax-AI#20, so it must not merge before MiniMax-AI#20's Hooks contract is accepted. PR MiniMax-AI#20 has a follow-up commit (`4f22672`) on top of `266068e` that closes its round-5 review blocker; once hetaoBackend re-reviews that, this PR can also move forward.
Summary
Companion proposal to
proposals/hooks.md(commitd86625d, hetaoBackend) that records thetwelve-event catalog, decision semantics, and field vocabulary actually shipped in
@minimax-ai/code@0.2.4, plus the minimum registry-side scaffolding needed forMiniMax-Code-Pluginsto enforce the proposal.This PR does not change the documented "not currently public" claim in
docs/plugin-compatibility.md. Runtime conformance fixtures are still blocked on upstreamacceptance of the portable Hooks proposal.
Why a companion rather than an extension of
d86625dd86625dis the primary portable proposal and was authored 2026-08-25 by the upstreammaintainer. Two design differences from the empirical 0.2.4 runtime emerged when surveying
cli.js:d86625d(portable)cli.js)PreToolUsesemanticsdecision/reason/hookSpecificOutputPermissionRequestCLAUDEandCODEXbridging rulesThis proposal is additive. It does not propose a different namespace, a different
observe-only floor, or a different promotion path. It records the precision needed to write
the conformance fixtures
d86625ditself calls for.Changes
proposals/hooks-detailed-spec.md— companion proposal (~220 lines).examples/hello-mcode-hooks/— minimal Skill + one experimentalio.minimax.mcode/hooks/hooks.jsonentry. SKILL.md, README, and the script each disclose: no credentials, no network, no
telemetry, no third-party services.
scripts/lib/validation.mjs— newvalidateClientExtensions,validateHooksDocument,validateHookEntry. Recognizes theio.minimax.mcodeextension namespace statically; noPlugin code is ever executed. Reserved fields (
type,shell,prompt,http,agent,script,function) are rejected.PLUGIN_ROOTandPLUGIN_DATAremain reserved inenv. Path safety mirrors the existing MCP stdio rules.test/validation.test.mjs— 5 new tests covering: field vocabulary, document shape,happy-path discovery, missing-extension tolerance, and rejection of unknown events.
Test evidence
Full suite (
npm test): 114/115 pass. The single failure istest/hosted-plugins.test.mjs:15, a pre-existing Windows-only assertion that hardcodes POSIXpath separators in its regex; Linux CI is green. Not introduced by this PR.
npm run validatefails on Windows for every existing plugin in the repo because of apre-existing CRLF handling bug in
scripts/validate.mjs(text.startsWith("---" + "\n")failson files that git has converted to CRLF on checkout). Not introduced by this PR. The
new example was checked in as LF; on a Linux CI runner the validator passes.
Design compliance
capabilities in the manifest" test still passes — the root manifest cannot declare
hooks.PLUGIN_ROOTorPLUGIN_DATA.No host-absolute literals, no drive letters, no
/Users/or/home/paths.PLUGIN_DATA; theprevious state file is preserved on failure.
SKILL.md,plugin.jsondescription, andREADME.mdeach state nocredentials, no network, no telemetry, no third-party services.
Out of scope (intentionally)
This PR does not:
docs/plugin-compatibility.mdto claim Hooks support.README.mdto remove the "not advertised" note.d86625d.validate.mjschange to the hosted registry CI.Those follow-ups require the upstream maintainer's sign-off on the portable proposal and
runtime conformance evidence; they are explicitly out of scope here.
Refs
proposals/hooks.md(commitd86625d) — portable Hooks preview, 2026-08-25.@minimax-ai/code@0.2.4CHANGELOG — runtime release notes, 2026-08-24.docs/plugin-compatibility.md.docs/security-model.md.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.